<?php

namespace app\common\service;

use app\common\exception\CatchException;
use app\common\util\EnvHelper;
use think\exception\ValidateException;
use think\facade\Request;
use think\Service as ThinkService;
use think\Validate;

/**
 * 服务基类
 */
class Service extends ThinkService
{
    protected $exceptionClass = CatchException::class;
    protected $isExceptionClassChanged = false;
    protected $params = [];

    /**
     * 类实例化（单例模式）
     *
     * @return static
     */
    public static function instance()
    {
        static $_instance = array();

        $classFullName = get_called_class();
        if (!isset($_instance[$classFullName])) {
            // $_instance[$classFullName] = new $classFullName();
            // 1、先前这样写的话，PhpStrom 代码提示功能失效；
            // 2、并且中间变量不能是 数组，如 不能用 return $_instance[$classFullName] 形式返回实例对象，否则 PhpStrom 代码提示功能失效；
            $instance = $_instance[$classFullName] = new static(app());
            return $instance;
        }

        return $_instance[$classFullName];
    }

    /**
     * 默认参数
     *
     * @return mixed
     */
    protected function params($validator = null)
    {
        $params = [];
        if ($this->params) {
            $params = $this->params;
        } else {
            $params = Request::instance()->param('', null, 'trim');
        }
        // 尝试校验
        if ($validator) {
            $this->validate($params, $validator);
        }
        return $params;
    }

    /**
     * 使用该方法需要先调用overrideParams或autoParams
     *
     * @param mixed $default 默认值
     * @param string|int ...$index 数组下标
     *
     * @return mixed
     */
    protected function pgd($default = null, ...$index)
    {
        $params = $this->params();
        foreach ($index as $i) {
            if (isset($params[$i])) {
                return $params[$i];
            }
        }
        return $default;
    }

    /**
     * 使用该方法需要先调用overrideParams
     *
     * @param string|int ...$index 数组下标
     *
     * @return mixed
     */
    protected function pg(...$index)
    {
        $params = $this->params();
        foreach ($index as $i) {
            if (isset($params[$i])) {
                return $params[$i];
            }
        }
        return null;
    }

    /**
     * 获取一个必须的参数
     */
    protected function req(...$index)
    {
        $params = $this->params();
        foreach ($index as $i) {
            if (isset($params[$i])) {
                return $params[$i];
            }
        }
        $desc = implode('或', $index);
        throw $this->exception("缺少参数 $desc");
    }

    /**
     * 获取一个类型必须为array的参数
     * @return array
     */
    protected function array(...$index)
    {
        $arg = $this->pg(...$index);
        if (is_null($arg)) {
            $arg = [];
        }
        if (!is_array($arg)) {
            $desc = implode('或', $index);
            throw $this->exception("参数 $desc 必须为数组");
        }
        return $arg;
    }

    /**
     * 获取主键
     *
     * @param string $name
     *
     * @return mixed
     */
    protected function primaryKey($name = 'id')
    {
        $params = $this->params(\think\facade\Validate::rule([$name => 'require']));
        return $params[$name];
    }

    /**
     * 通过主键获取对象
     * 如果找不到，请检查以下信息：
     * - 后端
     * - - 数据库中数据是否被删除
     * - - 多对一join时是否注意使用field限定id
     * - - 后端使用了错误模型查询
     * - - 配置了错误的id字段
     * - 前端
     * - - 前端请求了错误的资源接口
     * - - 错误的服务器地址
     * - - 错误的id字段
     * - - 传错id
     * @template T of \think\Model
     * @param class-string<T> $objClass 模型类
     * @param string $name 参数名
     * @param bool $trashed 是否使用被软删除的数据
     * @param bool $canBeEmpty 是否可为空
     *
     * @return T
     */
    protected function one(string $objClass, $name = 'id', $trashed = false, $canBeEmpty = false)
    {
        $id = $this->primaryKey($name);
        // 如果已经是一个对应对象，那么返回
        if ($id instanceof $objClass) {
            return $id;
        }
        $query = (new $objClass);
        if ($trashed) {
            /**
             * @var \think\model\concern\SoftDelete $query
             */
            $query->onlyTrashed();
        }
        $obj = $query->findOrEmpty($id);
        if (!$canBeEmpty && $obj?->isEmpty()) {
            throw $this->exception("未找到对象 $objClass $name=$id", 404);
        }
        return $obj;
    }

    /**
     * 覆盖params
     * - 优先级 function($params) > overrideParams($params) > autoget
     *
     * @param mixed $params
     *
     * @return Service
     */
    public function overrideParams($params)
    {
        $this->params = $params;
        return $this;
    }

    /**
     * 自动获取参数
     *
     * @param mixed $params 为空时自动获取参数，否则使用该参数
     *
     * @return array
     */
    protected function autoParams($params)
    {
        $this->params = $params ?: $this->params();
        return $this->params;
    }

    /**
     * 采用自动参数，采用交集,建议用于替换 autoParams
     * @param $params
     * @return array
     */
    protected function autoMergeParams($params)
    {
        $this->params=$this->params();
        if ($params) {
            $this->params = array_merge($this->params, $params);
        }
        return $this->params;
    }

    /**
     * 分页参数
     *
     * @param int $max
     *
     * @return array
     */
    public function pageParams($max = 100, $defaultSize = 10)
    {
        $this->validate($this->params, \think\facade\Validate::rule([
            'pageParams.page' => '>=:0',
            'pageParams.size' => "<=:{$max}"
        ]));
        $pageParams = $this->req('pageParams');
        $pageParams['size'] = $pageParams['size'] ?? $defaultSize;
        $pageParams['page'] = $pageParams['page'] ?? 1;
        return $pageParams;
    }

    /**
     * thinkphp6分页参数
     *
     * @param int $max 最大页大小
     * @param int $defaultSize 默认页大小
     *
     * @return array
     */
    public function tp6Page($max = 100, $defaultSize = 10)
    {
        ['page' => $page, 'size' => $size] = $this->pageParams($max, $defaultSize);
        return [
            'page' => $page,
            'list_rows' => $size ?: $defaultSize,
        ];
    }

    /**
     * 校验
     *
     * @param mixed $data
     * @param Validate $validator
     * @param array $msg
     *
     * @return bool
     */
    public function validate($data, Validate $validator)
    {
        $debug = EnvHelper::getDevDebug();
        if (!$validator->batch($debug)->check($data)) {
            throw new ValidateException($validator->getError());
        }
        return true;
    }

    /**
     * 设置或获取内置异常类
     *
     * @param string $class 异常类
     *
     * @return self|string
     */
    public function exceptionClass($class = null)
    {
        if (empty($class)) {
            return $this->exceptionClass;
        }
        if ($this->isExceptionClassChanged) {
            return $this;
        }
        $this->exceptionClass = $class;
        return $this;
    }

    /**
     * 锁定异常类
     *
     * @return self
     */
    public function lockException()
    {
        $this->isExceptionClassChanged = true;
        return $this;
    }

    /**
     * 封装异常
     *
     * @param \Exception $e 父异常
     * @param string $prefixMessage 消息前缀
     * @param  $newCode 新的code
     * @param string $class 如有需要使用另一个class
     *
     * @return \Exception
     */
    public function warpException(\Exception $e, $prefixMessage = "发生异常：", $newCode = null, $class = null)
    {
        $class = $class ?: $this->exceptionClass;
        $newCode = $newCode ?? $e->getCode();
        return new $class("{$prefixMessage}[{$e->getCode()}]：" . $e->getMessage(), $newCode, $e);
    }

    /**
     * 附带新异常
     *
     * @param \Closure $func 函数
     * @param mixed ...$args 参数
     *
     * @return void
     */
    public function withException(\Closure $func, ...$args)
    {
        try {
            call_user_func_array($func, $args);
        } catch (\Exception $e) {
            throw $this->warpException($e);
        }
    }

    /**
     * 使用内定的异常类
     *
     * @param string $message 消息
     * @param $code code
     * @param \Exception $parent 父异常
     * @param string $class 如有需要使用另一个class
     *
     * @return \Exception
     */
    public function exception($message = '', $code = -1, $parent = null, $class = null)
    {
        $class = $class ?: $this->exceptionClass;
        return new $class($message, $code, $parent);
    }
}
